Updating Data

Updating Data

Introduction

Relay 는 정규화된 GraphQL 데이터의 로컬 인메모리 store 를 보유해요.

이 store 는 어플리케이션 전반의 GraphQL query 를 수행할 때 데이터를 축적해요.

GraphQL 데이터의 로컬 데이터베이스라고 생각하면 돼요.

레코드를 업데이트 했을 때, 업데이트한 데이터의 영향을 받는 모든 컴포넌트에 전파하여 업데이트한 데이터와 함께 re-render 를 수행해요.

이 섹션에서는 서버에서 데이터를 업데이트 한 뒤, 뒤이어 로컬 데이터 저장소를 업데이트하여 컴포넌트가 최신 데이터와 동기화를 유지하는 방법에 대해 설명할 예정이에요.

GraphQL Mutations

GraphQL 에서는 GraphQL Mutation 을 이용하여 서버 데이터를 업데이트해요.

Mutations 는 서버에서 읽기-쓰기(read-write) 작업을 수행해요. Mutations 는 Back-end 에서 데이터를 수정하면서, 수정한 데이터를 동일 request 에서 query 하기 때문이에요.

Mutation 작성하기

GraphQL Mutation 은 query 작업과 아주 유사해요. mutation 키워드를 사용한다는 점을 제외하면요.

mutation FeedbackLikeMutation($input: FeedbackLikeData!) {
feedback_like(data: $input) {
feedback {
id
viewer_does_like
like_count
}
}
}
  • 예시의 mutation 은 특정 Feedback 오브젝트의 like 데이터를 수정해요. feedback_like 는 mutation root field(또는 mutation field) 에요. 이 mutation field 는 특정 data 를 입력받고 Back-end 에서 관련 데이터를 서버에서 업데이트 해요.
  • mutation 은 서로 분리된 두 단계로 나뉘어요. 먼저 서버에서 데이터를 업데이트해요. 그 후 query 를 수행해요. 이것으로 mutation response 부분만 업데이트한 데이터만 볼 수 있게 해줘요.
  • mutation field (예제에서는 feedback_like) 는 특정 GraphQL 타입을 반환해요. 이 GraphQL 타입은, mutation response 으로 query 할 수 있는 데이터를 노출시켜요.
  • mutation field 에서 접근할 수 있는 field 들은 일반적인 query 에서 접근할 수 있는 field 들을 알아서 자동으로 담지 않아요. 그래서 mutation response 에서 업데이트한 모든 entities 와 viewer 오브젝트를 mutation field 에 포함하는 것이 가장 좋은 방법이에요.
  • 예제를 보면 like_count 와, 현재 화면을 보고 있는 사용자가 Feedback object 를 좋아요 보여주는 viewer_does_like 값을 업데이트한 Feedback 오브젝트를 query 해요.

예제의 mutation 의 성공 response 는 다음과 같아요.

{
"feedback_like": {
"feedback": {
"id": "feedback-id",
"viewer_does_like": true,
"like_count": 1,
}
}
}

Relay 에서는 graphql 키워드를 사용해서 GraphQL mutations 를 선언할 수도 있어요.

const {graphql} = require('react-relay');
const feedbackLikeMutation = graphql`
mutation FeedbackLikeMutation($input: FeedbackLikeData!) {
feedback_like(data: $input) {
feedback {
id
viewer_does_like
like_count
}
}
}
`;

mutations 은 query 나 fragment 에서 사용하는 것처럼 variables 를 사용할 수 있어요.

Relay 에서는 서버에서 mutation 을 수행하기 위해, commitMutationuseMutation API 를 사용할 수 있어요.

아래는 commitMutation API 를 사용한 예제에요.

import type {Environment} from 'react-relay';
import type {FeedbackLikeData, FeedbackLikeMutation} from 'FeedbackLikeMutation.graphql';
const {commitMutation, graphql} = require('react-relay');
function commitFeedbackLikeMutation(
environment: Environment,
input: FeedbackLikeData,
) {
return commitMutation<FeedbackLikeMutation>(environment, {
mutation: graphql`
mutation FeedbackLikeMutation($input: FeedbackLikeData!) {
feedback_like(data: $input) {
feedback {
id
viewer_does_like
like_count
}
}
}
`,
variables: {input},
onCompleted: response => {} /* Mutation 성공시 */,
onError: error => {} /* Mutation 예외 발생시 */,
});
}
module.exports = {commit: commitFeedbackLikeMutation};

위 예제가 어떻게 작동하는지 자세히 확인해볼게요.

  • commitMutation 은 첫번째 인자로 environment 를 받아요. 두번째 인자에서는 graphql 키워드로 mutation 을 선언하고, mutation 요청을 위한 variables 를 선언해요.
  • inputFeedbackLikeMutation.graphql 모듈을 이용해 auto-gen 된 Flow 타입이 될 수 있어요. Relay 는 기본적으로 빌드시점에 mutation 을 위한 Flow 타입을 제네레이트 해요. 제네레이트 결과로 나온 Flow 타입은 *<mutation_name>*.graphql.js 형태를 띄어요.
  • variablesonCompletedresponse 매개변수, optimisticResponse 는 개별로 auto-gen 된 타입으로 타이핑되어요. FeedbackLikeMutation.graphql 모듈을 통해 FeedbackLikeMutation 타입으로 타이핑 된 것처럼.
  • optimisticResponse 필드를 타이핑하기 위해 mutation query root 에 @raw_response_type directive 를 할 수 있어요.
  • commitMutation 는 다음 두 콜백을 가져요. 요청을 성공적으로 완료한 경우 호출하는 onCompletd 와 에러가 발생했을 때 호출하는 onError.
  • mutation response 를 받으면, local store 에서 같은 id field 를 가진 레코드를 찾아 이 레코드를 mutation 응답의 새로운 field 로 자동으로 업데이트해요.
    • 예제에서는, local store 에 이미 존재하는 Feedback 오브젝트 중에서 mutation 응답으로 받은 id 와 매칭하는 Feedback 오브젝트를 찾아요. 그리고 이 매칭하는 Feedback 오브젝트에서 viewer_does_likelike_count 필드를 업데이트해요.
  • mutation response 를 통해 local store 의 데이터를 업데이트하면, 이 데이터를 구독하는 컴포넌트에 데이터 변화를 전파하고 re-render 를 발생시켜요.

요청이 성공했을 때의 데이터 업데이트

요청이 성공했을 때 store 데이터를 업데이트하는 방법으로 다음 네 가지가 있어요.

  • 내부 mutation field 로 id field 를 가지고 field 를 query 하면, local store 의 레코드는 mutation response 의 새로운 값으로 자동 업데이트해요. 위 예제에서는, query 가 feedbackid field 를 가지고 있기 때문에, Relay 는 local store 에서 이 id field 와 매칭한 Feedback 을 찾아요. 그리고 Feedback 오브젝트에서 viewer_does_likelike_count field 를 업데이트해요.
    • mutation 을 완료한 후, fragment 를 refetch 하는 대신, mutation response 를 가지고 fragment 을 spread 해요. 이 방법을 통해 동일 요청에서 fragment 데이터 또한 업데이트 할 수 있어요.
  • 내부 mutation field 로 id field 와 @deleteRecord directive 를 가지고 있으면, local store 에서 해당 field 를 삭제해요.
  • 내부 mutation field 로 @prepandEdge@appendEdge directive 를 가지고 edge field 를 query 하면, connection 에서 edge 를 prepend 하거나 append 해요.
  • 위 세 가지에 해당하지 않으면, mutation 요청이 성공했을 때 local store 의 데이터를 어떻게 update 할지는 updater 콜백으로 설정할 수 있어요.

지금까지 local store 의 데이터를 업데이트하는 방법을 개별 시나리오로만 표현했어요. 그런데 둘 이상의 방법으로 local store 데이터를 업데이트하는 경우, relay 가 어떤 순서로 데이터를 업데이트하는지 아래에 updater 함수들의 실행 순서 에서 자세히 확인할 수 있어요.

Updater 함수

단순히 필드값을 업데이트하거나 mutation directive 를 선언하는 것만으로는 부족해서 좀 더 복잡한 작업을 진행해서 업데이트하길 원한다면 commitMutation 이나 useMutationupdater 함수를 선언해서 store 업데이트하는 것을 전부 관리할 수 있어요.

import type {Environment} from 'react-relay';
import type {CommentCreateData, CreateCommentMutation} from 'CreateCommentMutation.graphql';
const {commitMutation, graphql} = require('react-relay');
const {ConnectionHandler} = require('relay-runtime');
function commitCommentCreateMutation(
environment: Environment,
feedbackID: string,
input: CommentCreateData,
) {
return commitMutation<CreateCommentMutation>(environment, {
mutation: graphql`
mutation CreateCommentMutation($input: CommentCreateData!) {
comment_create(input: $input) {
comment_edge {
cursor
node {
body {
text
}
}
}
}
}
`,
variables: {input},
onCompleted: () => {},
onError: error => {},
updater: store => {
const feedbackRecord = store.get(feedbackID);
// record 를 구해요
const connectionRecord = ConnectionHandler.getConnection(
feedbackRecord,
'CommentsComponent_comments_connection',
);
// 서버 응답에서 payload 를 구해요
const payload = store.getRootField('comment_create');
// payload 에서 edge 값을 구해요
const serverEdge = payload.getLinkedRecord('comment_edge');
// connection 에 추가하기 위한 edge 를 생성해요
const newEdge = ConnectionHandler.buildConnectionEdge(
store,
connectionRecord,
serverEdge,
);
// connection 끝에 edge 를 추가해요
ConnectionHandler.insertEdgeAfter(
connectionRecord,
newEdge,
);
},
});
}
module.exports = {commit: commitCommentCreateMutation};

예제에 대해 자세히 살펴볼게요.

  • updater 함수는 RecordSourceSelectorProxy 의 인스턴스인 store 를 첫번째 인자로 받아요. 이 interface 는 절차적으로 Relay store 의 데이터를 읽고 작성해요. 이것은 mutation response 의 응답에 store 를 업데이트 하는 방식을 개발자가 전부 제어할 수 있음을 의미해요. 개발자는 새로운 record 를 전적으로 생성할 수도 있고, 기존 record 를 업데이트하거나 삭제할 수 있어요.
    • updater 함수는 두번째 인자로 payload 를 받아요. paylod 인자는 mutation response 오브젝트에요. payload 인자를 통해서 store 에 접근하지 않아도 mutation response 로 받은 payload data 를 읽을 수 있어요.
  • 예제를 볼게요. 서버에 comment 를 성공적으로 추가한 이후, local store 에 새로운 comment 를 추가했어요. 더 자세히 이야기하자면. connection 에 새로운 item 을 추가하는 거에요. connection 에서 item 을 추가하거나 삭제하는 방법에 대해 좀 더 자세히 알고 싶다면 이 섹션 을 참고해주세요.
    • 사실 위의 예제에서는 굳이 updater 함수를 사용하지 않아도 괜찮아요! 예제 상황에서는 @appendEdge directive 를 사용하는 것이 best practice 에요.
  • mutation response 는 store 로부터 접근 가능한 root field record 라는 점을 기억하세요. store.getRootField API 를 사용해서 접근할 수 있어요. 예제에서는 mutation response root field 인 comment_create root field 에 접근하고 있어요.
  • mutation 의 root field 는 query 의 root field 와 구분해서 생각해야 돼요. mutation updater 의 store.getRootField 는 mutation response 의 record 에요. mutation response 뿐만이 아닌 전체 root 에서 record 에 접근하고 싶다면 store.getRoot().getLinkedRecord 를 대신 사용해요.
  • mutation 의 updater 함수를 통해 local store 의 데이터를 업데이트하면, 이 데이터를 구독하는 컴포넌트에 데이터 변화를 전파하고 re-render 를 발생시켜요.

낙관적(optimistic) update

사용자 인터렉션에 응답하기 전에 먼저 서버 응답부터 기다려야 하는 것을 피하고 싶을 수 있어요. 예를 들어 사용자가 "좋아요" 버튼을 클릭하면, 해당 포스트에 "좋아요" 를 했다고 바로 보여주는 거에요. 서버로부터 mutation response 를 아직 받지 않았는데도요. 이번 섹션에서 곧 어떻게 이것을 구현할 수 있는지 다룰거에요.

더 일반적인 상황에 맞추어 설명해볼게요. 인지 반응성(perceived responsiveness) 를 향상시키기 위해 local 데이터를 낙관적으로 즉시 업데이트 하고 싶은 상황이에요. 즉 mutation 이 성공하면 바로 반영하리라 생각하는 것을 local data 에 즉시 업데이트하고 싶은 거에요.

물론 mutation 이 실패하면 에러 메시지를 보여주면서 롤백 할 수 있어요. 하지만 대부분의 mutation 은 성공할 것이라고 낙관적으로 기대한다는 점을 떠올려봐요.

이 낙관적 업데이트를 구현하기 위해 Relay 는 mutation 을 실행하면서 낙관적 업데이트를 진행할 수 있도록 두 개의 API 를 제공해요.

낙관적 응답(Optimistic Response)

mutation 에 대한 서버 응답이 올 것을 예상하고 store 에 낙관적 업데이트를 먼저 진행하기 위한 가장 간단한 방법이 있어요. 바로 commitMutationoptimisticResponse 를 선언하는 거에요.

import type {Environment} from 'react-relay';
import type {FeedbackLikeData, FeedbackLikeMutation} from 'FeedbackLikeMutation.graphql';
const {commitMutation, graphql} = require('react-relay');
function commitFeedbackLikeMutation(
environment: Environment,
feedbackID: string,
input: FeedbackLikeData,
) {
return commitMutation<FeedbackLikeMutation>(environment, {
mutation: graphql`
mutation FeedbackLikeMutation($input: FeedbackLikeData!)
@raw_response_type {
feedback_like(data: $input) {
feedback {
id
viewer_does_like
}
}
}
`,
variables: {input},
optimisticResponse: {
feedback_like: {
feedback: {
id: feedbackID,
viewer_does_like: true,
},
},
},
onCompleted: () => {} /* Mutation 성공 */,
onError: error => {} /* Mutation 실패 */,
});
}
module.exports = {commit: commitFeedbackLikeMutation};

예제에서 어떤 일이 발생하는지 알아볼까요?

  • optimisticResponse 는 mutation response 형태와 동일한 형태를 가진 오브젝트에요. optimisticResponse 는 서버로부터 성공적인 응답이 왔다고 시뮬레이트해요. optimisticResponse 을 선언했다면 Relay 는 서버 응답을 처리하는 것과 동일한 방식으로 optimisticResponse 응답을 처리해요. 그 후 optimisticResponse 응답에 따라 데이터를 업데이트해요.
    • 예제를 더 살펴볼게요. 주어진 feedbackId 와 일치하는 feedback 을 찾아서 record 를 업데이트 할 거에요. Feedback 오브젝트에서 viewer_does_lik 를 즉시 true 로 변경하고, 이 데이터의 변경을 곧바로 UI 화면에 반영할 거에요.
  • mutation 이 성공적으로 응답한다면, 방금 진행했던 낙관적 업데이트를 롤백하면서 서버 응답으로 대체해요.
  • mutaiton 이 실패한 경우, 낙관적 업데이트를 롤백하고 onError 콜백에서 정의한 action (예: 에러 메시지 출력) 을 수행할거에요.
  • GraphQL schema 에 @raw_response_type directive 를 선언한 경우 optimisticResponse 를 위한 타입을 generate 한다는 점을 기억하세요.

낙관적 Updater

하지만 서버 응답이 정적으로 예측 가능한 것이 아닐 수 있어요. 또 좀 더 복잡한 업데이트를 수행하면서 낙관적 업데이트를 수행해야 할 수 있어요. 예를 들어 record 를 삭제한 뒤 새로 생성하거나 connection 에 item 을 추가하거나 삭제하는 것 같은 복잡한 업데이트에 낙관적 업데이트를 적용하고 싶을 수 있어요.

이럴 때에는 otimisticUpdater 함수를 commitMutation 에 선언해요. 예를 들어볼게요. optimisticResponse 대신 optimisticUpdater 를 사용하면 viewer_does_like 를 true 로 설정한 뒤, like_count field 를 증가시킬 수 있어요.

import type {Environment} from 'react-relay';
import type {FeedbackLikeData} from 'FeedbackLikeMutation.graphql';
const {commitMutation, graphql} = require('react-relay');
function commitFeedbackLikeMutation(
environment: Environment,
feedbackID: string,
input: FeedbackLikeData,
) {
return commitMutation(environment, {
mutation: graphql`
mutation FeedbackLikeMutation($input: FeedbackLikeData!) {
feedback_like(data: $input) {
feedback {
id
like_count
viewer_does_like
}
}
}
`,
variables: {input},
optimisticUpdater: store => {
// Feedback 오브젝트를 위한 record 를 구해요
const feedbackRecord = store.get(feedbackID);
// like_count 의 현재 값을 읽어요.
const currentLikeCount = feedbackRecord.getValue('like_count');
// 낙관적으로 like_count 의 값을 1 증가시켜요
feedbackRecord.setValue((currentLikeCount ?? 0) + 1, 'like_count');
// 낙관적으로 viewer_does_like 를 true 로 설정해요
feedbackRecord.setValue(true, 'viewer_does_like');
},
onCompleted: () => {} /* Mutation 성공 */,
onError: error => {} /* Mutation 실패 */,
});
}
module.exports = {commit: commitFeedbackLikeMutation};

예제에 대해 좀 더 자세히 볼게요.

  • optimisticUpdater 는 일반적인 updater 함수와 같은 시그니처를 가지고 동일하게 작동해요. 하지만 optimisticUpdater 은 mutation response 를 완료하기 전에 즉시 실행하는 함수라는 점이 큰 차이점이에요.
  • 만약 mutation 이 성공한 경우, 낙관적 업데이트를 롤백하고, 서버 응답으로 대체해요.
    • optimisticResponse 를 사용했다면 like_count 값을 어떤 고정된 값으로 정적으로 업데이트할 수 없어요. 그래서 like_count 값을 현재 값에서 1 만큼 증가시키기 위해서는 먼저 store 에서 현재의 like_count 값을 알아야 해요. optimisticUpdater 를 사용하면 이러한 작업이 가능해요.
    • mutation 을 완료했을 때, 서버에서 받은 값은 낙관적으로 업데이트한 값과 차이를 보일 수 있어요. 예제의 경우, 만약 현재 화면을 보고 있는 사용자가 아닌 다른 누군가가 "좋아요" 를 했다면, 낙관적 업데이트는 1만큼 증가시키지만 실제 서버로부터 데이터를 반영했을 때는 2만큼 증가할 수 있다는 이야기에요.
  • mutation 이 실패하면, 낙관적 업데이트는 롤백하고 onError 콜백에서 정의한 action 을 수행해요.
  • optimisticUpdater 와는 별개로 updater 함수를 선언하지 않았다면, 서버 응답이 도착하면 기본 작업을 수행해요. 예제에서는 Feedback 오브젝트의 like_countviewer_does_like 값을 업데이트 할거에요.

알아두세요

mutation 으로 local store 의 데이터를 업데이트하면, 이 데이터를 구독하는 컴포넌트에 데이터 변화를 전파하고 re-render 를 발생시켜요.

updater 함수들의 실행 순서

updater 함수와 낙관적 업데이트는 아래 순서처럼 작동해요.

  • optimisticResponse 를 선언했다면, Relay 는 optimisticResponse 에서 업데이트해야 하는 record 를 찾아서 새로운 값으로 업데이트해요.
  • optimisticUpdater 를 선언했다면, Relay 는 optimisticUpdater 에서 정의내린 대로 store 를 업데이트해요.
  • optimsiticResponse 를 선언했다면, 낙관적 업데이트를 진행하면서 @deleteRecord, @appendEdge, @prependEdge mutation directive 를 수행해요.
  • 만약 mutation 이 성공했다면 다음 과정을 진행해요.
    • 낙관적 업데이트를 롤백해요.
    • Relay 는 서버 응답에서 업데이트해야 하는 record 를 찾고 새로운 값으로 업데이트해요.
    • updater 를 선언했다면, Relay 는 updater 함수가 정의내린대로 store 를 업데이트해요. 서버로부터 받은 payload 는 updater 함수에서 store 의 root field 로 이용 가능해요.
    • @deleteRecord, @appendEdge, @prependEdge mutation directive 를 수행해요.
  • 만약 mutation 이 실패했다면 다음 과정을 진행해요.
    • 낙관적 업데이트를 롤백해요.
    • onError 콜백을 호출해요.

전체 예제

다음 예제는 사용할 수 있는 모든 옵션들(optimisticResponse, optimisticUpdater, updater) 을 이용해 복잡한 시나리오를 구현한 예제에요.

새로운 comment 를 추가하는 mutation 을 나타낸 예제에요. (connection 업데이트에 대한 자세한 설명은 connection 업데이트하기 를 참고하세요.)

import type {Environment} from 'react-relay';
import type {CommentCreateData, CreateCommentMutation} from 'CreateCommentMutation.graphql';
const {commitMutation, graphql} = require('react-relay');
const {ConnectionHandler} = require('relay-runtime');
function commitCommentCreateMutation(
environment: Environment,
feedbackID: string,
input: CommentCreateData,
) {
return commitMutation<CreateCommentMutation>(environment, {
mutation: graphql`
mutation CreateCommentMutation($input: CommentCreateData!) {
comment_create(input: $input) {
feedback {
id
viewer_has_commented
}
comment_edge {
cursor
node {
body {
text
}
}
}
}
}
`,
variables: {input},
onCompleted: () => {},
onError: error => {},
// 낙관적으로 `viewer_has_commented` 를 업데이트하기 위한 값을 설정해요
optimisticResponse: {
feedback: {
id: feedbackID,
viewer_has_commented: true,
},
},
// 낙관적으로 comments connection 에 새로운 comment 를 추가해요
optimisticUpdater: store => {
const feedbackRecord = store.get(feedbackID);
const connectionRecord = ConnectionHandler.getConnection(
userRecord,
'CommentsComponent_comments_connection',
);
// 완전 기초부터 local comment 를 생성해요
const id = `client:new_comment:${randomID()}`;
const newCommentRecord = store.create(id, 'Comment');
// ... content 와 함께 새로운 comment 를 업데이트해요
// 완전 기초부터 새로운 edge 를 생성해요
const newEdge = ConnectionHandler.createEdge(
store,
connectionRecord,
newCommentRecord,
'CommentEdge' /* GraphQl Type for edge */,
);
// connection 의 끝에 edge 를 추가해요
ConnectionHandler.insertEdgeAfter(connectionRecord, newEdge);
},
updater: store => {
const feedbackRecord = store.get(feedbackID);
const connectionRecord = ConnectionHandler.getConnection(
userRecord,
'CommentsComponent_comments_connection',
);
// 서버에서 payload 를 받아요
const payload = store.getRootField('comment_create');
// server payload 에서 edge 를 읽어요
const newEdge = payload.getLinkedRecord('comment_edge');
// connection 의 끝에 edge 를 추가해요
ConnectionHandler.insertEdgeAfter(connectionRecord, newEdge);
},
});
}
module.exports = {commit: commitCommentCreateMutation};

updater 함수들을 실행하는 순서에 따라 예제를 살펴 볼게요.

  • optimisticResponse 를 선언했기 때문에 가장 먼저 optimisticResponse 를 수행해요. local store 의 $Feedback 오브젝트에서 viewer_has_commented 필드를 true 로 낙관적 업데이트를 진행해요.
  • optimisticUpdater 를 선언했기 때문에 이어서 optimisticUpdater 을 수행해요. optimisticUpdater 는 함수 내부에서 완전히 기초부터 새로운 comment 와 edge record 를 생성해요. 생성을 완료하면 connection 에 새로운 edge 를 추가해요.
  • 낙관적 업데이트를 완료하면, 이 데이터들을 구독하고 있는 컴포넌트에 데이터가 변경되었다는 점을 전파해요.
  • mutation 이 성공했다면 모든 낙관적 업데이트를 롤백해요.
  • Relay 는 서버로부터 응답받은 데이터로 local store 의 Feedback 오브젝트에서 viewer_has_commented 값을 true 로 변경해요.
  • 마지막으로, updater 함수를 실행해요. updater 함수는 optimisticUpdater 함수와 비슷하지만, 새로운 데이터를 완전히 기초부터 생성하지 않고 mutation response 의 payload 에서 새로운 데이터를 읽어온 뒤 그 데이터를 가지고 connection 에 edge 를 추가해요.

mutation 도중 데이터 무효화

mutation 과 관련한 모든 데이터를 mutation 과정의 일부로 서버로부터 다시 받아오는 것이 best practice 에요. 이를 통해 Relay local store 는 서버와 동일한 상태를 유지할 수 있어요.

하지만 '사용자 차단' 이나 '그룹 이탈' 과 같이 파급 효과가 큰 mutation 에 영향받는 데이터들을 전부 특정하는 것이 불가능할 때가 있어요.

이와 같은 mutation 상황에서는, 전체 store 나 일부 데이터를 명시적으로 stale 로 표시하여 다음 렌더링 때 Relay 가 re-fetch 하도록 할 수 있어요.

이와 같이 데이터 무효화 API 와 관련한 이야기는 데이터 섹션의 부패(staleness) 에서 더 자세히 확인할 수 있어요.

Mutation queueing

TBD

GraphQL Subscriptions

GraphQL Subscriptions 는 클라이언트가 서버의 데이터 변경을 구독해서 서버 데이터의 변경이 발생할 때마다 이것을 전파받는 메커니즘이다.

GraphQL Subscriptions 는 query 와 유사한 형태를 가지지만 subscription 키워드를 사용한다는 차이점이 있다.

subscription FeedbackLikeSubscription($input: FeedbackLikeSubscribeData!) {
feedback_like_subscribe(data: $input) {
feedback {
id
like_count
}
}
}
  • subscription 을 구독하면 Feedback 오브젝트에서 "좋아요" 나 "좋아요" 취소가 발생할 때 클라이언트에게 이러한 데이터 변경 사실을 전달해요. feedback_like_subscription field 는 백엔드에서 특정 입력을 받고 구독 설정을 하는 subscription field 그 자체에요.
  • feedback_like_subscription 는 특정 GraphQL 타입을 반환해요. 이 GraphQL 타입은, subscription payload 으로 query 할 수 있는 데이터를 노출시켜요. 다시 말해, 클라이언트는 서버의 데이터 변경을 전달 받으면서 subscription payload 를 받아요. 예제에서는 업데이트한 like_count 상태를 반영한 Feedback 오브젝트를 query 해요. like_count 는 실시간으로 좋아요 숫자를 반영해요.

클라이언트가 받은 subscription payload 의 형태는 다음과 같아요.

{
"feedback_like_subscribe": {
"feedback": {
"id": "feedback-id",
"like_count": 321
}
}
}

Relay 에서는 graphql 키워드를 사용해서 GraphQL subscrition 을 선언할 수 있어요.

const {graphql} = require('react-relay');
const feedbackLikeSubscription = graphql`
subscription FeedbackLikeSubscription($input: FeedbackLikeSubscribeData!) {
feedback_like_subscribe(data: $input) {
feedback {
id
like_count
}
}
}
`;
  • subscription 은 query 나 fragment 와 같은 방식으로 GraphQL variables 를 참조할 수 있다는 점을 기억하세요.

서버에서 subscription 을 수행하는 데에는 두 가지 방법이 있어요. requestSubscription API 와 hook 을 사용하는 거에요.

subscription API 요청하기

Relay 가 서버에서 subscription 을 수행하기 위해 requestSubscription API 를 사용해요

import type {Environment} from 'react-relay';
import type {FeedbackLikeSubscribeData} from 'FeedbackLikeSubscription.graphql';
const {graphql, requestSubscription} = require('react-relay');
function feedbackLikeSubscribe(
environment: Environment,
feedbackID: string,
input: FeedbackLikeSubscribeData,
) {
return requestSubscription(environment, {
subscription: graphql`
subscription FeedbackLikeSubscription(
$input: FeedbackLikeSubscribeData!
) {
feedback_like_subscribe(data: $input) {
feedback {
id
like_count
}
}
}
`,
variables: {input},
onCompleted: () => {} /* Subscription 완료 */,
onError: error => {} /* Subscription 에러 */,
onNext: response => {} /* Subscription payload 구독 */
});
}
module.exports = {subscribe: feedbackLikeSubscribe};

예제를 살펴볼게요

  • requestSubscription 는 enviroment 인자를 받아요. graphql 키워드로 subscription 을 정의 내릴 수 있고 variables 을 사용할 수 있어요.
  • inputFeedbackLikeSubscription.graphql 모듈을 이용해 auto-gen 된 Flow 타입이 될 수 있어요. Relay 는 기본적으로 빌드시점에 mutation 을 위한 Flow 타입을 제네레이트 해요. 제네레이트 결과로 나온 Flow 타입은 *<subscription_name>*.graphql.js 형태를 띄어요.
  • requestSubscription 에서는 onCompletdonError 콜백을 선언할 수 있어요. 각각 subscription 을 완료하거나 에러가 발생했을 때 사용해요.
  • requestSubscription 에서는 onNext 콜백을 선언할 수 있어요. subscription payload 가 갱신 될 때마다 호출해요.
  • subscription payload 를 받으면 subscription payload 의 오브젝트는 id 를 가지고, local store 에서 id 와 짝이 맞는 record 를 찾아 새로운 field 값으로 업데이트해요. 예제에서는 local store 에서 subscription payload 의 id 와 동일한 id 를 가진 Feedback 를 찾아서 like_count field 를 업데이트해요.
  • subscription 를 통해 local store 의 데이터를 업데이트하면, 이 데이터를 구독하는 컴포넌트에 데이터 변화를 전파하고 re-render 를 발생시켜요.

subscription 의 결과로 local 데이터를 업데이트할 때 단순히 field 를 업데이트하는 것보다 더 복잡한 작업을 수행하고 싶을 수 있어요. 예를 들어 기존 record 를 삭제하고 새로운 record 를 생성하거나 connection 에서 새로운 item 을 추가하고 삭제하는 것과 같은 작업이에요. 이런 복잡한 업데이트를 수행하기 위해 requestSubscriptionupdater 함수를 선언해서 store 에 대한 업데이트를 전부 제어할 수 있어요.

import type {Environment} from 'react-relay';
import type {CommentCreateSubscribeData} from 'CommentCreateSubscription.graphql';
const {graphql, requestSubscription} = require('react-relay');
function commentCreateSubscribe(
environment: Environment,
feedbackID: string,
input: CommentCreateSubscribeData,
) {
return requestSubscription(environment, {
subscription: graphql`
subscription CommentCreateSubscription(
$input: CommentCreateSubscribeData!
) {
comment_create_subscribe(data: $input) {
feedback_comment_edge {
cursor
node {
body {
text
}
}
}
}
}
`,
variables: {input},
updater: store => {
const feedbackRecord = store.get(feedbackID);
// Get connection record
const connectionRecord = ConnectionHandler.getConnection(
feedbackRecord,
'CommentsComponent_comments_connection',
);
// 서버 응답에서 payload 를 구해요
const payload = store.getRootField('comment_create_subscribe');
// payload 에서 edge 값을 구해요
const serverEdge = payload.getLinkedRecord('feedback_comment_edge');
// connection 에 추가하기 위한 edge 를 생성해요
const newEdge = ConnectionHandler.buildConnectionEdge(
store,
connectionRecord,
serverEdge,
);
// connection 끝에 edge 를 추가해요
ConnectionHandler.insertEdgeAfter(connectionRecord, newEdge);
},
onCompleted: () => {} /* Subscription 완료 */,
onError: error => {} /* Subscription 실패 */,
onNext: response => {} /* Subscription payload 구독 */,
});
}
module.exports = {subscribe: commentCreateSubscribe};

예제를 자세히 살펴볼게요

  • updater 함수는 RecordSourceSelectorProxy 의 인스턴스인 store 를 첫번째 인자로 받아요. 이 interface 는 절차적으로 Relay store 의 데이터를 읽고 작성해요. 이것은 subscription payload 의 응답에 store 를 업데이트 하는 방식을 개발자가 전부 제어할 수 있음을 의미해요. 개발자는 새로운 record 를 전적으로 생성할 수도 있고, 기존 record 를 업데이트하거나 삭제할 수 있어요. Relay store 가 읽고 쓰는 작업에 대한 전체 API 는 https://facebook.github.io/relay/docs/en/relay-store.html 에서 확인 가능해요.
  • 예제를 볼게요. subscription payload 를 받은 뒤 local store 에 새로운 comment 를 추가했어요. 더 자세히 이야기하자면. connection 에 새로운 item 을 추가하는 거에요. connection 에서 item 을 추가하거나 삭제하는 방법에 대해 좀 더 자세히 알고 싶다면 이 섹션 을 참고해주세요.
  • subscription payload 는 store 로부터 접근 가능한 root field record 라는 점을 기억하세요. store.getRootField API 를 사용해서 접근할 수 있어요. 예제에서는 subscription response root field 인 comment_create_subsribe root field 에 접근하고 있어요.
  • updater 함수를 통해 local store 의 데이터를 업데이트하면, 이 데이터를 구독하는 컴포넌트에 데이터 변화를 전파하고 re-render 를 발생시켜요.

hook 을 사용해서 subscription 요청하기

subscription query 를 구독하기 위해 hook 을 사용할 수 있어요.

import {graphql, useSubscription} from 'react-relay';
import {useMemo} from 'react';
const subscription = graphql`subscription ...`;
function MyFunctionalComponent({ id }) {
// 중요: config 는 memoized 된 상태이거나 최소 매 render 때마다 재평가 되지는 않아요.
// 중요: 그렇게 하지 않으면, usbSubscription 은 너무 자주 re-render 를 발생시킬 수 있어요.
const config = useMemo(() => { variables: { id }, subscription }, [id]);
useSubscription(config);
return <div>Move Fast</div>
}

이 방법은 requestSubscription API 를 간단히 wrapping 한 형태에요. 다음과 같이 작동해요.

  • 컴포넌트를 mount 할 때, 주어진 config 를 가지고 subscribe 를 수행해요.
  • 컴포넌트를 unmount 할 때, unsubscribe 를 수행해요.

subscription 을 요청하는데 어떤 복잡하고 절차적인 작업을 수행해야 한다면 requestSubscription API 를 직접 사용하는 것이 좋아요.

Network Layer 설정하기

subscription 을 다루기 위해 Network layer 를 설정할 수 있어요.

기본적으로 GraphQL subscription 은 WebSockets 을 통해 통신해요. 다음은 graphql-ws 를 사용한 예제애요.

import {
...
Network,
Observable
} from 'relay-runtime';
import { createClient } from 'graphql-ws';
const wsClient = createClient({
url:'ws://localhost:3000',
});
const subscribe = (operation, variables) => {
return Observable.create((sink) => {
return wsClient.subscribe(
{
operationName: operation.name,
query: operation.text,
variables,
},
sink,
);
});
}
const network = Network.create(fetchQuery, subscribe);
...

subscriptions-transport-ws 를 사용한 예제도 있어요.

import {
...
Network,
Observable
} from 'relay-runtime';
import { SubscriptionClient } from 'subscriptions-transport-ws';
...
const subscriptionClient = new SubscriptionClient('ws://localhost:3000', {
reconnect: true,
});
const subscribe = (request, variables) => {
const subscribeObservable = subscriptionClient.request({
query: request.text,
operationName: request.name,
variables,
});
// 중요: subscriptions-transport-ws observable 타입을 Relay 의 observable 타입으로 변경해요
return Observable.from(subscribeObservable);
};
const network = Network.create(fetchQuery, subscribe);
...

Local 데이터 업데이트

Relay store 에서 local 데이터만을 업데이트하기 위한 몇 가지 API 들이 있어요. 서버와는 무관하게 업데이트하는 상황 등에 사용할 수 있어요.

local 데이터 업데이트와 관련한 API 는 client-only data 와 서버에 fetch 하여 데이터를 받아오는 일반적인 데이터 업데이트 양쪽 모두 사용이 가능해요.

commitLocalUpdate

commitLocalUpdate API 에 updater 콜백을 전달해서 local 데이터를 업데이트 할 수 있어요.

import type {Environment} from 'react-relay';
const {commitLocalUpdate, graphql} = require('react-relay');
function commitCommentCreateLocally(
environment: Environment,
feedbackID: string,
) {
return commitLocalUpdate(environment, store => {
const feedbackRecord = store.get(feedbackID);
const connectionRecord = ConnectionHandler.getConnection(
userRecord,
'CommentsComponent_comments_connection',
);
// 완전 기초부터 local Comment 생성
const id = `client:new_comment:${randomID()}`;
const newCommentRecord = store.create(id, 'Comment');
// ... content 를 통해 local Comment 업데이트
// 완전 기초부터 새로운 edge 생성
const newEdge = ConnectionHandler.createEdge(
store,
connectionRecord,
newCommentRecord,
'CommentEdge' /* GraphQl Type for edge */,
);
// connect 의 끝에 edge 를 추가한다
ConnectionHandler.insertEdgeAfter(connectionRecord, newEdge);
});
}
module.exports = {commit: commitCommentCreateLocally};
  • commitLocalUpdate 는 environment 를 첫번째 인자로 받고, updater 콜백 함수를 두번째 인자로 받아요.
    • updater 콜백 함수는 RecordSourceSelectorProxy 의 인스턴스인 store 를 인자로 받아요. 이 interface 는 절차적으로 Relay store 의 데이터를 읽고 작성해요. 이것은 store 를 업데이트 하는 방식을 개발자가 전부 제어할 수 있음을 의미해요. 개발자는 새로운 record 를 전적으로 생성할 수도 있고, 기존 record 를 업데이트하거나 삭제할 수 있어요.
  • 예제에서는 local store 에 새로운 comment 를 추가해요. 좀 더 구체적으로 설명하면 connection 에 새로운 item 을 추가하는 것으로 볼 수 있어요. connection 에서 item 을 추가하거나 삭제하는 방법에 대해 좀 더 자세히 알고 싶다면 이 섹션 을 참고해주세요.
  • local store 의 데이터를 업데이트하면, 이 데이터를 구독하는 컴포넌트에 데이터 변화를 전파하고 re-render 를 발생시켜요.

commitPayload

commitPayloadOperationDescriptor 와 query 에 대한 payload 를 인자로 받고 Relay store 을 업데이트해요.

payload 는 일반적인 상황에서 서버가 query 에 응답하는 것처럼 resolved 되고,

JSResource, requireDefer 등으로 전달되는 데이터 기반 종속성(Data Driven Dependencies) 또한 resolve 해요.

import type {FooQueryRawResponse} from 'FooQuery.graphql'
const {createOperationDescriptor} = require('relay-runtime');
const operationDescriptor = createOperationDescriptor(FooQuery, {
id: 'an-id',
otherVariable: 'value',
});
const payload: FooQueryRawResponse = {
me: {
id: '4',
name: 'Zuck',
profilePicture: {
uri: 'https://...',
},
},
};
environment.commitPayload(operationDescriptor, payload);
  • createOperationDescriptor 는 query 와 variables 를 인자로 받아 OperationDescriptor 를 반환해요.
  • @raw_response_type directive 를 query 에 추가해서 payload 에 대한 Flow type 를 generate 할 수 있어요.
  • local store 의 데이터를 업데이트하면, 이 데이터를 구독하는 컴포넌트에 데이터 변화를 전파하고 re-render 를 발생시켜요.

Client-Only Data

Client-Only Data (Client Schema Extension)

Relay 는 client schema 확장을 통해, 브라우저와 같은 클라이언트에서 GraphQL schema 를 확장하는 기능을 제공해요.

이런 기능을 제공하는 건 클라이언트에서만 읽고, 쓰고, 업데이트할 필요가 있는 데이터를 모델링 하기 위해서에요.

서버에서 가져온 데이터에 좀 더 정보를 추가하거나, Relay 가 저장하고 관리할 클라이언트 특화 상태(client-specific state)를 전체적으로 모델링하는 데 유용할 수 있어요ㅏ.

client schema 확장을 사용하면 schema field 에 새로운 field 를 추가해서 기존 type 을 수정하거나,

client 에서만 존재하는 완전히 새로운 type 을 생성할 수 있어요.

기존 type 확장하기

기존 type 을 확장하기 위해, --src 같은 적당한 디렉터리에 .graphql 파일을 추가해요.

extend type Comment {
is_new_comment: Boolean
}

Comment type 이 있어요.

컴포넌트에서는 이 Comment 타입을 읽어서 render 하고 Relay API 를 이용하면 이 Comment 타입을 업데이트 할 수 있어요.

예제에서는 extend 키워드를 사용해서 이 기존의 Comment type 을 확장하고 있어요.

이렇게 Comment type 을 확장하면,

어떤 comment 가 새로 추가되었을 때, 기존에는 구현할 수 없었던 새로운 시각적 요소를 추가하여 컴포넌트를 render 할 수 있어요.

새로운 Type 추가하기

html/js/relay/schema/ 디렉토리에 .graphql 파일을 생성하고 GraphQL 문법을 사용해서 새로운 type 을 정의할 수 있어요.

# 하나의 파일에 여러 type 을 정의할 수 있어요.
enum FetchStatus {
FETCHED
PENDING
ERRORED
}
type FetchState {
# 다른 타입을 정의하기 위해 client type 들을 재사용할 수 있어요.
status: FetchStatus
# 일반적인 server type 들을 참조할 수도 있어요
started_by: User!
}
extend type Item {
# Server Type 을 client-only type 을 사용해서 확장할 수 있어요
fetch_state: FetchState
}
  • 다소 인위적인 예시지만, 두 개의 client-only type 과 enum, 일반적인 type 을 정의했어요. 이 type 들은 자기자신들을 참조할 수도 있고, server 에서 정의한 일반적인 type 을 참조할 수도 있어요. 또 server type 에 client-only type 을 추가해서 확장하는 것도 가능해요.
  • 앞서 언급했듯, Relay API 를 이용해서 이 데이터들을 정상적으로 읽고 쓰는 것이 가능해요.

Client-Only 데이터 읽기

fragment 나 query 내부에서 다른 일반적인 데이터에 접근하듯, Client-only 데이터에도 접근할 수 있어요.

const data = useFragment(
graphql`
fragment CommentComponent_comment on Comment {
# 다른 field 접근하듯 client-only field 에도 접근이 가능해요
is_new_comment
body {
text
}
}
`,
props.user,
);

Client-Only 데이터 업데이트 하기

client-only 데이터를 업데이트하기 위해, 일반적으로 사용하는 mutation 이나 subscription updater 들을 사용할 수도 있고, local 데이터를 업데이트하는 데 사용하는 API 들도 마찬가지로 사용 가능해요.